package jmine.tec.database.query.services;

import java.security.PrivateKey;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

import jmine.tec.database.query.DatabaseUtil;
import jmine.tec.di.annotation.Injected;
import jmine.tec.services.annotations.Execution;
import jmine.tec.services.annotations.Input;
import jmine.tec.services.annotations.ServiceImplementor;
import jmine.tec.services.annotations.Validation;
import jmine.tec.services.api.ActionsEnum;
import jmine.tec.services.api.ServiceAssertionFailedError;
import jmine.tec.services.api.ServiceProperties;
import jmine.tec.services.api.ServiceValidationExecutionException;
import jmine.tec.services.impl.ServicesImplMessages;
import jmine.tec.utils.cripto.TokenUtil;
import jmine.tec.utils.cripto.exception.CriptoException;
import jmine.tec.utils.cripto.key.PrivateKeyLocator;
import jmine.tec.utils.cripto.key.impl.ClasspathPrivateKeyLocator;

import org.hibernate.Session;
import org.hibernate.jdbc.Work;

import bancosys.tec.persist.controller.PersistenceEnabledController;

/**
 * Serviço para execução de um comando SQL. Recebe o comando a ser executado encriptado por uma chave privada que deve ser mantido secreta,
 * deve ser utilizado como último recurso.
 * <p/>
 * <p/>
 * <p/>
 * Utilizar como último recurso.
 * <p/>
 * <p/>
 * <p/>
 * 
 * @author lundberg
 */
@ServiceImplementor(action = ActionsEnum.EXECUTAR)
public class ComandoEncriptadoService {

    public static final String COMANDO_ENCRIPTADO = "Comando encriptado";

    public static final String COMANDO_TEXTO_PLANO = "Comando em texto plano";

    public static final String TIPO_COMANDO = "Tipo de comando";

    public static final String ATUALIZACOES_ESPERADAS = "Atualizações esperadas";

    private static final String PRIVATE_KEY_NAME = "encryptedCommandService.key";

    private PersistenceEnabledController controller;

    private ServiceProperties properties;

    private CommandType commandType;

    private Integer expectedUpdatedRows;

    /**
     * {@inheritDoc}
     */
    @Validation
    public void encryptedShouldBeDecriptable() {
        if (this.getEncryptedCommand() != null) {
            try {
                this.getDecryptedCommand();
            } catch (CriptoException e) {
                throw new ServiceValidationExecutionException(
                        ServicesImplMessages.DATABASE_EXECUTAR_CYPHEREDCOMMAND_COMANDO_ENCRIPTADO_INVALIDO.create());
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Validation
    public void plainAndEncryptedShouldBeSame() {
        if (this.getPlainTextCommand() != null && !this.getPlainTextCommand().equals(this.getDecryptedCommand())) {
            throw new ServiceValidationExecutionException(
                    ServicesImplMessages.DATABASE_EXECUTAR_CYPHEREDCOMMAND_COMANDO_DECRIPTADO_NAO_COINCIDE_COM_TEXTO_PLANO.create());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Execution
    public void execute() {
        Session session = this.getSession();
        final String decryptedCommand = this.getDecryptedCommand();
        if (this.commandType.isUpdate()) {
            session.doWork(new Work() {
                public void execute(Connection connection) throws SQLException {
                    Statement st = null;
                    try {
                        st = connection.createStatement();
                        ComandoEncriptadoService.this.validateUpdatedRows(st.executeUpdate(decryptedCommand));
                    } finally {
                        DatabaseUtil.closeQuietly(st);
                    }
                }
            });
        } else {
            session.doWork(new Work() {
                public void execute(Connection connection) throws SQLException {
                    CallableStatement st = null;
                    try {
                        st = connection.prepareCall(decryptedCommand);
                        st.execute();
                    } finally {
                        DatabaseUtil.closeQuietly(st);
                    }
                }
            });
        }
    }

    /**
     * Valida que o número de linhas atualizadas corresponde ao esperado.
     * 
     * @param updatedRows updatedRows
     */
    protected void validateUpdatedRows(int updatedRows) {
        if (this.expectedUpdatedRows != null && !this.expectedUpdatedRows.equals(updatedRows)) {
            throw new ServiceAssertionFailedError(
                    ServicesImplMessages.DATABASE_EXECUTAR_CYPHEREDCOMMAND_NUMERO_LINHAS_ATUALIZADAS_NAO_ESPERADO.create(
                            this.expectedUpdatedRows, updatedRows));
        }
    }

    /**
     * Obtém uma referência à sessão atual
     * 
     * @return session
     */
    private Session getSession() {
        return this.controller.getSessionFactory().getCurrentSession();
    }

    /**
     * Obtém o comando encriptado
     * 
     * @return string
     */
    private String getEncryptedCommand() {
        return this.properties.getRaw(COMANDO_ENCRIPTADO);
    }

    /**
     * Obtém o comando decriptado
     * 
     * @return string
     */
    private String getDecryptedCommand() {
        PrivateKeyLocator locator = new ClasspathPrivateKeyLocator();
        PrivateKey key = locator.getPrivateKey(PRIVATE_KEY_NAME);
        return TokenUtil.decripta(key, this.getEncryptedCommand());
    }

    /**
     * Obtém o comando em texto plano.
     * 
     * @return string
     */
    private String getPlainTextCommand() {
        return this.properties.getRaw(COMANDO_TEXTO_PLANO);
    }

    /**
     * @param controller the controller to set
     */
    @Injected
    public void setController(PersistenceEnabledController controller) {
        this.controller = controller;
    }

    /**
     * @param properties the properties to set
     */
    @Injected
    public void setProperties(ServiceProperties properties) {
        this.properties = properties;
    }

    /**
     * Tipo de comando a ser executado.
     * 
     * @param commandType commandType
     */
    @Input(fieldName = TIPO_COMANDO, defaultValue = "UPDATE")
    public void setCommandType(CommandType commandType) {
        this.commandType = commandType;
    }

    /**
     * Número de linhas atualizadas esperadas, utilizada para validar a execução de updates/deletes/inserts
     * 
     * @param expectedUpdatedRows expectedUpdatedRows
     */
    @Input(fieldName = ATUALIZACOES_ESPERADAS, required = false)
    public void setExpectedUpdatedRows(Integer expectedUpdatedRows) {
        this.expectedUpdatedRows = expectedUpdatedRows;
    }

    /**
     * Setter "marcador" da necessidade do campo existir.
     * 
     * @param encryptedCommand the encryptedCommand to set
     */
    @Input(fieldName = COMANDO_ENCRIPTADO)
    public void setEncryptedCommand(String encryptedCommand) {

    }

}
